*/ public function register(): array { return [ T_DOC_COMMENT_OPEN_TAG, ]; } /** * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint * @param int $docCommentOpenPointer */ public function process(File $phpcsFile, $docCommentOpenPointer): void { $tokens = $phpcsFile->getTokens(); $tokenCodes = [T_VARIABLE, T_FOREACH, T_WHILE, T_LIST, T_OPEN_SHORT_ARRAY]; $commentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer']; $codePointer = TokenHelper::findFirstNonWhitespaceOnNextLine($phpcsFile, $commentClosePointer); if ($codePointer === null || !in_array($tokens[$codePointer]['code'], $tokenCodes, true)) { $firstPointerOnPreviousLine = TokenHelper::findFirstNonWhitespaceOnPreviousLine($phpcsFile, $docCommentOpenPointer); if ( $firstPointerOnPreviousLine === null || !in_array($tokens[$firstPointerOnPreviousLine]['code'], $tokenCodes, true) ) { return; } $codePointer = $firstPointerOnPreviousLine; } /** @var list> $variableAnnotations */ $variableAnnotations = AnnotationHelper::getAnnotations($phpcsFile, $docCommentOpenPointer, '@var'); if (count($variableAnnotations) === 0) { return; } foreach (array_reverse($variableAnnotations) as $variableAnnotation) { if ($variableAnnotation->isInvalid()) { continue; } $variableName = $variableAnnotation->getValue()->variableName; if ($variableName === '') { continue; } $variableAnnotationType = $variableAnnotation->getValue()->type; if ( $variableAnnotationType instanceof UnionTypeNode || $variableAnnotationType instanceof IntersectionTypeNode ) { foreach ($variableAnnotationType->types as $typeNode) { if (!$this->isValidTypeNode($typeNode)) { continue 2; } } } elseif (!$this->isValidTypeNode($variableAnnotationType)) { continue; } /** @var IdentifierTypeNode|ThisTypeNode|UnionTypeNode|GenericTypeNode $variableAnnotationType */ $variableAnnotationType = $variableAnnotationType; $assertion = $this->createAssert($variableName, $variableAnnotationType); if ($assertion === null) { continue; } if ($tokens[$codePointer]['code'] === T_VARIABLE) { $pointerAfterVariable = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1); if ($tokens[$pointerAfterVariable]['code'] !== T_EQUAL) { continue; } if ($variableName !== $tokens[$codePointer]['content']) { continue; } $pointerToAddAssertion = $this->getNextSemicolonInSameScope($phpcsFile, $codePointer, $codePointer + 1); $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer); } elseif ($tokens[$codePointer]['code'] === T_LIST) { $listParenthesisOpener = TokenHelper::findNextEffective($phpcsFile, $codePointer + 1); $variablePointerInList = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $listParenthesisOpener + 1, $tokens[$listParenthesisOpener]['parenthesis_closer'], ); if ($variablePointerInList === null) { continue; } $pointerToAddAssertion = $this->getNextSemicolonInSameScope($phpcsFile, $codePointer, $codePointer + 1); $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer); } elseif ($tokens[$codePointer]['code'] === T_OPEN_SHORT_ARRAY) { $pointerAfterList = TokenHelper::findNextEffective($phpcsFile, $tokens[$codePointer]['bracket_closer'] + 1); if ($tokens[$pointerAfterList]['code'] !== T_EQUAL) { continue; } $variablePointerInList = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $codePointer + 1, $tokens[$codePointer]['bracket_closer'], ); if ($variablePointerInList === null) { continue; } $pointerToAddAssertion = $this->getNextSemicolonInSameScope( $phpcsFile, $codePointer, $tokens[$codePointer]['bracket_closer'] + 1, ); $indentation = IndentationHelper::getIndentation($phpcsFile, $docCommentOpenPointer); } else { if ($tokens[$codePointer]['code'] === T_WHILE) { $variablePointerInWhile = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer'], ); if ($variablePointerInWhile === null) { continue; } $pointerAfterVariableInWhile = TokenHelper::findNextEffective($phpcsFile, $variablePointerInWhile + 1); if ($tokens[$pointerAfterVariableInWhile]['code'] !== T_EQUAL) { continue; } } else { $asPointer = TokenHelper::findNext( $phpcsFile, T_AS, $tokens[$codePointer]['parenthesis_opener'] + 1, $tokens[$codePointer]['parenthesis_closer'], ); $variablePointerInForeach = TokenHelper::findNextContent( $phpcsFile, T_VARIABLE, $variableName, $asPointer + 1, $tokens[$codePointer]['parenthesis_closer'], ); if ($variablePointerInForeach === null) { continue; } } $pointerToAddAssertion = $tokens[$codePointer]['scope_opener']; $indentation = IndentationHelper::addIndentation($phpcsFile, IndentationHelper::getIndentation($phpcsFile, $codePointer)); } $fix = $phpcsFile->addFixableError( 'Use assertion instead of inline documentation comment.', $variableAnnotation->getStartPointer(), self::CODE_REQUIRED_EXPLICIT_ASSERTION, ); if (!$fix) { continue; } $phpcsFile->fixer->beginChangeset(); FixerHelper::removeBetweenIncluding($phpcsFile, $variableAnnotation->getStartPointer(), $variableAnnotation->getEndPointer()); $docCommentUseful = false; $docCommentClosePointer = $tokens[$docCommentOpenPointer]['comment_closer']; for ($i = $docCommentOpenPointer + 1; $i < $docCommentClosePointer; $i++) { $tokenContent = trim($phpcsFile->fixer->getTokenContent($i)); if ($tokenContent === '' || $tokenContent === '*') { continue; } $docCommentUseful = true; break; } $pointerBeforeDocComment = TokenHelper::findPreviousContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $docCommentOpenPointer - 1, ); $pointerAfterDocComment = TokenHelper::findNextContent( $phpcsFile, T_WHITESPACE, $phpcsFile->eolChar, $docCommentClosePointer + 1, ); if (!$docCommentUseful) { FixerHelper::removeBetweenIncluding($phpcsFile, $pointerBeforeDocComment + 1, $pointerAfterDocComment); } if ( $pointerToAddAssertion < $docCommentClosePointer && array_key_exists($pointerAfterDocComment + 1, $tokens) ) { FixerHelper::addBefore($phpcsFile, $pointerAfterDocComment + 1, $indentation . $assertion . $phpcsFile->eolChar); } else { FixerHelper::add($phpcsFile, $pointerToAddAssertion, $phpcsFile->eolChar . $indentation . $assertion); } $phpcsFile->fixer->endChangeset(); } } private function isValidTypeNode(TypeNode $typeNode): bool { if ($typeNode instanceof ThisTypeNode) { return true; } if ($typeNode instanceof IdentifierTypeNode) { return true; } if ( $this->enableIntegerRanges && $typeNode instanceof GenericTypeNode && $typeNode->type->name === 'int' && count($typeNode->genericTypes) === 2 ) { foreach ($typeNode->genericTypes as $genericType) { $isValid = ($genericType instanceof IdentifierTypeNode && in_array($genericType->name, ['min', 'max'], true)) || ($genericType instanceof ConstTypeNode && $genericType->constExpr instanceof ConstExprIntegerNode); if (!$isValid) { return false; } } return true; } return false; } private function getNextSemicolonInSameScope(File $phpcsFile, int $scopePointer, int $searchAt): int { $semicolonPointer = null; do { $semicolonPointer = TokenHelper::findNext($phpcsFile, T_SEMICOLON, $searchAt); if (ScopeHelper::isInSameScope($phpcsFile, $scopePointer, $semicolonPointer)) { break; } $searchAt = $semicolonPointer + 1; } while (true); return $semicolonPointer; } /** * @param IdentifierTypeNode|ThisTypeNode|UnionTypeNode|IntersectionTypeNode|GenericTypeNode $typeNode */ private function createAssert(string $variableName, TypeNode $typeNode): ?string { $conditions = []; if ( $typeNode instanceof IdentifierTypeNode || $typeNode instanceof ThisTypeNode || $typeNode instanceof GenericTypeNode ) { $conditions = $this->createConditions($variableName, $typeNode); return $conditions !== [] ? sprintf('\assert(%s);', implode(' || ', $conditions)) : null; } /** @var IdentifierTypeNode|ThisTypeNode|GenericTypeNode $innerTypeNode */ foreach ($typeNode->types as $innerTypeNode) { $innerTypeConditions = $this->createConditions($variableName, $innerTypeNode); if ($innerTypeConditions === []) { return null; } $conditions = array_merge($conditions, $innerTypeConditions); } $operator = $typeNode instanceof IntersectionTypeNode ? '&&' : '||'; $formattedConditions = []; foreach (array_unique($conditions) as $condition) { $formattedConditions[] = $operator === '||' && strpos($condition, '&&') !== false ? sprintf('(%s)', $condition) : $condition; } return sprintf('\assert(%s);', implode(sprintf(' %s ', $operator), $formattedConditions)); } /** * @param IdentifierTypeNode|ThisTypeNode|GenericTypeNode $typeNode * @return list */ private function createConditions(string $variableName, TypeNode $typeNode): array { if ($typeNode instanceof GenericTypeNode) { $conditions = [sprintf('\is_int(%s)', $variableName)]; if ($typeNode->genericTypes[0] instanceof ConstTypeNode) { $conditions[] = sprintf('%s >= %s', $variableName, (string) $typeNode->genericTypes[0]); } if ($typeNode->genericTypes[1] instanceof ConstTypeNode) { $conditions[] = sprintf('%s <= %s', $variableName, (string) $typeNode->genericTypes[1]); } return [implode(' && ', $conditions)]; } if ($typeNode instanceof ThisTypeNode) { return [sprintf('%s instanceof $this', $variableName)]; } if ($typeNode->name === 'self') { return [sprintf('%s instanceof %s', $variableName, $typeNode->name)]; } if ($typeNode->name === 'static') { return [sprintf('%s instanceof static', $variableName)]; } if (in_array($typeNode->name, ['true', 'false', 'null'], true)) { return [sprintf('%s === %s', $variableName, $typeNode->name)]; } if ( $typeNode->name === 'mixed' || TypeHintHelper::isVoidTypeHint($typeNode->name) || TypeHintHelper::isNeverTypeHint($typeNode->name) ) { return []; } if (TypeHintHelper::isSimpleTypeHint($typeNode->name)) { return [sprintf('\is_%s(%s)', TypeHintHelper::convertLongSimpleTypeHintToShort($typeNode->name), $variableName)]; } if (in_array($typeNode->name, ['resource', 'object'], true)) { return [sprintf('\is_%s(%s)', $typeNode->name, $variableName)]; } if ($typeNode->name === 'numeric') { return [ sprintf('\is_numeric(%s)', $variableName), ]; } if ($typeNode->name === 'scalar') { return [ sprintf('\is_int(%s)', $variableName), sprintf('\is_float(%s)', $variableName), sprintf('\is_bool(%s)', $variableName), sprintf('\is_string(%s)', $variableName), ]; } if ($this->enableIntegerRanges) { if ($typeNode->name === 'positive-int' || $typeNode->name === 'non-negative-int') { return [sprintf('\is_int(%1$s) && %1$s > 0', $variableName)]; } if ($typeNode->name === 'negative-int' || $typeNode->name === 'non-positive-int') { return [sprintf('\is_int(%1$s) && %1$s < 0', $variableName)]; } if ($typeNode->name === 'literal-int') { return [sprintf('\is_int(%1$s)', $variableName)]; } } if ( $this->enableAdvancedStringTypes && preg_match('~-string$~', $typeNode->name) === 1 && preg_match('~^(?:class|trait|enum)-string$~', $typeNode->name) !== 1 ) { $conditions = [sprintf('\is_string(%s)', $variableName)]; if ($typeNode->name === 'callable-string') { $conditions[] = sprintf('\is_callable(%s)', $variableName); } elseif ($typeNode->name === 'numeric-string') { $conditions[] = sprintf('\is_numeric(%s)', $variableName); } elseif (preg_match('~^non-empty-~i', $typeNode->name) === 1) { $conditions[] = sprintf("%s !== ''", $variableName); } elseif (preg_match('~^non-falsy-~i', $typeNode->name) === 1) { $conditions[] = sprintf('(bool) %s === true', $variableName); } return [implode(' && ', $conditions)]; } if (TypeHintHelper::isSimpleUnofficialTypeHints($typeNode->name)) { return []; } return [sprintf('%s instanceof %s', $variableName, $typeNode->name)]; } }